home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2009 February / PCWFEB09.iso / Software / Resources / Chat & Communication / Digsby build 37 / digsby_setup.exe / lib / lxml / html / diff.pyo (.txt) < prev    next >
Encoding:
Python Compiled Bytecode  |  2008-10-13  |  17.8 KB  |  645 lines

  1. # Source Generated with Decompyle++
  2. # File: in.pyo (Python 2.5)
  3.  
  4. import difflib
  5. from lxml import etree
  6. from lxml.html import fragment_fromstring
  7. import cgi
  8. import re
  9. __all__ = [
  10.     'html_annotate',
  11.     'htmldiff']
  12.  
  13. try:
  14.     _unicode = unicode
  15. except NameError:
  16.     _unicode = str
  17.  
  18.  
  19. try:
  20.     basestring = __builtins__['basestring']
  21. except (KeyError, NameError):
  22.     basestring = str
  23.  
  24.  
  25. def default_markup(text, version):
  26.     return '<span title="%s">%s</span>' % (cgi.escape(_unicode(version), 1), text)
  27.  
  28.  
  29. def html_annotate(doclist, markup = default_markup):
  30.     tokenlist = [ tokenize_annotated(doc, version) for doc, version in doclist ]
  31.     cur_tokens = tokenlist[0]
  32.     for tokens in tokenlist[1:]:
  33.         html_annotate_merge_annotations(cur_tokens, tokens)
  34.         cur_tokens = tokens
  35.     
  36.     cur_tokens = compress_tokens(cur_tokens)
  37.     result = markup_serialize_tokens(cur_tokens, markup)
  38.     return ''.join(result).strip()
  39.  
  40.  
  41. def tokenize_annotated(doc, annotation):
  42.     tokens = tokenize(doc, include_hrefs = False)
  43.     for tok in tokens:
  44.         tok.annotation = annotation
  45.     
  46.     return tokens
  47.  
  48.  
  49. def html_annotate_merge_annotations(tokens_old, tokens_new):
  50.     s = InsensitiveSequenceMatcher(a = tokens_old, b = tokens_new)
  51.     commands = s.get_opcodes()
  52.     for command, i1, i2, j1, j2 in commands:
  53.         if command == 'equal':
  54.             eq_old = tokens_old[i1:i2]
  55.             eq_new = tokens_new[j1:j2]
  56.             copy_annotations(eq_old, eq_new)
  57.             continue
  58.     
  59.  
  60.  
  61. def copy_annotations(src, dest):
  62.     for src_tok, dest_tok in zip(src, dest):
  63.         dest_tok.annotation = src_tok.annotation
  64.     
  65.  
  66.  
  67. def compress_tokens(tokens):
  68.     result = [
  69.         tokens[0]]
  70.     for tok in tokens[1:]:
  71.         if not (result[-1].post_tags) and not (tok.pre_tags) and result[-1].annotation == tok.annotation:
  72.             compress_merge_back(result, tok)
  73.             continue
  74.         result.append(tok)
  75.     
  76.     return result
  77.  
  78.  
  79. def compress_merge_back(tokens, tok):
  80.     last = tokens[-1]
  81.     if type(last) is not token or type(tok) is not token:
  82.         tokens.append(tok)
  83.     else:
  84.         text = _unicode(last)
  85.         if last.trailing_whitespace:
  86.             text += ' '
  87.         
  88.         text += tok
  89.         merged = token(text, pre_tags = last.pre_tags, post_tags = tok.post_tags, trailing_whitespace = tok.trailing_whitespace)
  90.         merged.annotation = last.annotation
  91.         tokens[-1] = merged
  92.  
  93.  
  94. def markup_serialize_tokens(tokens, markup_func):
  95.     for token in tokens:
  96.         for pre in token.pre_tags:
  97.             yield pre
  98.         
  99.         html = token.html()
  100.         html = markup_func(html, token.annotation)
  101.         if token.trailing_whitespace:
  102.             html += ' '
  103.         
  104.         yield html
  105.         for post in token.post_tags:
  106.             yield post
  107.         
  108.     
  109.  
  110.  
  111. def htmldiff(old_html, new_html):
  112.     old_html_tokens = tokenize(old_html)
  113.     new_html_tokens = tokenize(new_html)
  114.     result = htmldiff_tokens(old_html_tokens, new_html_tokens)
  115.     result = ''.join(result).strip()
  116.     return fixup_ins_del_tags(result)
  117.  
  118.  
  119. def htmldiff_tokens(html1_tokens, html2_tokens):
  120.     s = InsensitiveSequenceMatcher(a = html1_tokens, b = html2_tokens)
  121.     commands = s.get_opcodes()
  122.     result = []
  123.     for command, i1, i2, j1, j2 in commands:
  124.         if command == 'equal':
  125.             result.extend(expand_tokens(html2_tokens[j1:j2], equal = True))
  126.             continue
  127.         
  128.         if command == 'insert' or command == 'replace':
  129.             ins_tokens = expand_tokens(html2_tokens[j1:j2])
  130.             merge_insert(ins_tokens, result)
  131.         
  132.         if command == 'delete' or command == 'replace':
  133.             del_tokens = expand_tokens(html1_tokens[i1:i2])
  134.             merge_delete(del_tokens, result)
  135.             continue
  136.     
  137.     result = cleanup_delete(result)
  138.     return result
  139.  
  140.  
  141. def expand_tokens(tokens, equal = False):
  142.     for token in tokens:
  143.         for pre in token.pre_tags:
  144.             yield pre
  145.         
  146.         if not equal or not (token.hide_when_equal):
  147.             if token.trailing_whitespace:
  148.                 yield token.html() + ' '
  149.             else:
  150.                 yield token.html()
  151.         
  152.         for post in token.post_tags:
  153.             yield post
  154.         
  155.     
  156.  
  157.  
  158. def merge_insert(ins_chunks, doc):
  159.     (unbalanced_start, balanced, unbalanced_end) = split_unbalanced(ins_chunks)
  160.     doc.extend(unbalanced_start)
  161.     if doc and not doc[-1].endswith(' '):
  162.         doc[-1] += ' '
  163.     
  164.     doc.append('<ins>')
  165.     if balanced and balanced[-1].endswith(' '):
  166.         balanced[-1] = balanced[-1][:-1]
  167.     
  168.     doc.extend(balanced)
  169.     doc.append('</ins> ')
  170.     doc.extend(unbalanced_end)
  171.  
  172.  
  173. class DEL_START:
  174.     pass
  175.  
  176.  
  177. class DEL_END:
  178.     pass
  179.  
  180.  
  181. class NoDeletes(Exception):
  182.     pass
  183.  
  184.  
  185. def merge_delete(del_chunks, doc):
  186.     doc.append(DEL_START)
  187.     doc.extend(del_chunks)
  188.     doc.append(DEL_END)
  189.  
  190.  
  191. def cleanup_delete(chunks):
  192.     while None:
  193.         
  194.         try:
  195.             (pre_delete, delete, post_delete) = split_delete(chunks)
  196.         except NoDeletes:
  197.             break
  198.  
  199.         (unbalanced_start, balanced, unbalanced_end) = split_unbalanced(delete)
  200.         locate_unbalanced_end(unbalanced_end, pre_delete, post_delete)
  201.         doc = pre_delete
  202.         if doc and not doc[-1].endswith(' '):
  203.             doc[-1] += ' '
  204.         
  205.         doc.append('<del>')
  206.         if balanced and balanced[-1].endswith(' '):
  207.             balanced[-1] = balanced[-1][:-1]
  208.         
  209.         doc.extend(balanced)
  210.         doc.append('</del> ')
  211.         doc.extend(post_delete)
  212.         chunks = doc
  213.         continue
  214.         return chunks
  215.  
  216.  
  217. def split_unbalanced(chunks):
  218.     start = []
  219.     end = []
  220.     tag_stack = []
  221.     balanced = []
  222.     for chunk in chunks:
  223.         if not chunk.startswith('<'):
  224.             balanced.append(chunk)
  225.             continue
  226.         
  227.         endtag = chunk[1] == '/'
  228.         name = chunk.split()[0].strip('<>/')
  229.         if name in empty_tags:
  230.             balanced.append(chunk)
  231.             continue
  232.         
  233.         if endtag:
  234.             if tag_stack and tag_stack[-1][0] == name:
  235.                 balanced.append(chunk)
  236.                 (name, pos, tag) = tag_stack.pop()
  237.                 balanced[pos] = tag
  238.             elif tag_stack:
  239.                 []([ tag for name, pos, tag in tag_stack ])
  240.                 tag_stack = []
  241.                 end.append(chunk)
  242.             else:
  243.                 end.append(chunk)
  244.         tag_stack[-1][0] == name
  245.         tag_stack.append((name, len(balanced), chunk))
  246.         balanced.append(None)
  247.     
  248.     []([ chunk for name, pos, chunk in tag_stack ])
  249.     balanced = _[3]
  250.     return (start, balanced, end)
  251.  
  252.  
  253. def split_delete(chunks):
  254.     
  255.     try:
  256.         pos = chunks.index(DEL_START)
  257.     except ValueError:
  258.         raise NoDeletes
  259.  
  260.     pos2 = chunks.index(DEL_END)
  261.     return (chunks[:pos], chunks[pos + 1:pos2], chunks[pos2 + 1:])
  262.  
  263.  
  264. def locate_unbalanced_start(unbalanced_start, pre_delete, post_delete):
  265.     while not unbalanced_start:
  266.         break
  267.     finding = unbalanced_start[0]
  268.     finding_name = finding.split()[0].strip('<>')
  269.     if not post_delete:
  270.         break
  271.     
  272.     next = post_delete[0]
  273.     if next is DEL_START or not next.startswith('<'):
  274.         break
  275.     
  276.     if next[1] == '/':
  277.         break
  278.     
  279.     name = next.split()[0].strip('<>')
  280.     if name == 'ins':
  281.         break
  282.     
  283.     if name == finding_name:
  284.         unbalanced_start.pop(0)
  285.         pre_delete.append(post_delete.pop(0))
  286.         continue
  287.     break
  288.     continue
  289.  
  290.  
  291. def locate_unbalanced_end(unbalanced_end, pre_delete, post_delete):
  292.     while not unbalanced_end:
  293.         break
  294.     finding = unbalanced_end[-1]
  295.     finding_name = finding.split()[0].strip('<>/')
  296.     if not pre_delete:
  297.         break
  298.     
  299.     next = pre_delete[-1]
  300.     if next is DEL_END or not next.startswith('</'):
  301.         break
  302.     
  303.     name = next.split()[0].strip('<>/')
  304.     if name == 'ins' or name == 'del':
  305.         break
  306.     
  307.     if name == finding_name:
  308.         unbalanced_end.pop()
  309.         post_delete.insert(0, pre_delete.pop())
  310.         continue
  311.     break
  312.     continue
  313.  
  314.  
  315. class token(_unicode):
  316.     hide_when_equal = False
  317.     
  318.     def __new__(cls, text, pre_tags = None, post_tags = None, trailing_whitespace = False):
  319.         obj = _unicode.__new__(cls, text)
  320.         if pre_tags is not None:
  321.             obj.pre_tags = pre_tags
  322.         else:
  323.             obj.pre_tags = []
  324.         if post_tags is not None:
  325.             obj.post_tags = post_tags
  326.         else:
  327.             obj.post_tags = []
  328.         obj.trailing_whitespace = trailing_whitespace
  329.         return obj
  330.  
  331.     
  332.     def __repr__(self):
  333.         return 'token(%s, %r, %r)' % (_unicode.__repr__(self), self.pre_tags, self.post_tags)
  334.  
  335.     
  336.     def html(self):
  337.         return _unicode(self)
  338.  
  339.  
  340.  
  341. class tag_token(token):
  342.     
  343.     def __new__(cls, tag, data, html_repr, pre_tags = None, post_tags = None, trailing_whitespace = False):
  344.         obj = token.__new__(cls, '%s: %s' % (type, data), pre_tags = pre_tags, post_tags = post_tags, trailing_whitespace = trailing_whitespace)
  345.         obj.tag = tag
  346.         obj.data = data
  347.         obj.html_repr = html_repr
  348.         return obj
  349.  
  350.     
  351.     def __repr__(self):
  352.         return 'tag_token(%s, %s, html_repr=%s, post_tags=%r, pre_tags=%r, trailing_whitespace=%s)' % (self.tag, self.data, self.html_repr, self.pre_tags, self.post_tags, self.trailing_whitespace)
  353.  
  354.     
  355.     def html(self):
  356.         return self.html_repr
  357.  
  358.  
  359.  
  360. class href_token(token):
  361.     hide_when_equal = True
  362.     
  363.     def html(self):
  364.         return 'Link: %s' % self
  365.  
  366.  
  367.  
  368. def tokenize(html, include_hrefs = True):
  369.     body_el = parse_html(html, cleanup = True)
  370.     chunks = flatten_el(body_el, skip_tag = True, include_hrefs = include_hrefs)
  371.     return fixup_chunks(chunks)
  372.  
  373.  
  374. def parse_html(html, cleanup = True):
  375.     if cleanup:
  376.         html = cleanup_html(html)
  377.     
  378.     return fragment_fromstring(html, create_parent = True)
  379.  
  380. _body_re = re.compile('<body.*?>', re.I | re.S)
  381. _end_body_re = re.compile('</body.*?>', re.I | re.S)
  382. _ins_del_re = re.compile('</?(ins|del).*?>', re.I | re.S)
  383.  
  384. def cleanup_html(html):
  385.     match = _body_re.search(html)
  386.     if match:
  387.         html = html[match.end():]
  388.     
  389.     match = _end_body_re.search(html)
  390.     if match:
  391.         html = html[:match.start()]
  392.     
  393.     html = _ins_del_re.sub('', html)
  394.     return html
  395.  
  396. end_whitespace_re = re.compile('[ \\t\\n\\r]$')
  397.  
  398. def fixup_chunks(chunks):
  399.     tag_accum = []
  400.     cur_word = None
  401.     result = []
  402.     for chunk in chunks:
  403.         if isinstance(chunk, tuple):
  404.             if chunk[0] == 'img':
  405.                 src = chunk[1]
  406.                 tag = chunk[2]
  407.                 if tag.endswith(' '):
  408.                     tag = tag[:-1]
  409.                     trailing_whitespace = True
  410.                 else:
  411.                     trailing_whitespace = False
  412.                 cur_word = tag_token('img', src, html_repr = tag, pre_tags = tag_accum, trailing_whitespace = trailing_whitespace)
  413.                 tag_accum = []
  414.                 result.append(cur_word)
  415.                 continue
  416.             if chunk[0] == 'href':
  417.                 href = chunk[1]
  418.                 cur_word = href_token(href, pre_tags = tag_accum, trailing_whitespace = True)
  419.                 tag_accum = []
  420.                 result.append(cur_word)
  421.                 continue
  422.             continue
  423.         
  424.         if is_word(chunk):
  425.             if chunk.endswith(' '):
  426.                 chunk = chunk[:-1]
  427.                 trailing_whitespace = True
  428.             else:
  429.                 trailing_whitespace = False
  430.             cur_word = token(chunk, pre_tags = tag_accum, trailing_whitespace = trailing_whitespace)
  431.             tag_accum = []
  432.             result.append(cur_word)
  433.             continue
  434.         if is_start_tag(chunk):
  435.             tag_accum.append(chunk)
  436.             continue
  437.         if is_end_tag(chunk):
  438.             if tag_accum:
  439.                 tag_accum.append(chunk)
  440.             else:
  441.                 cur_word.post_tags.append(chunk)
  442.         tag_accum
  443.     
  444.     if not result:
  445.         return [
  446.             token('', pre_tags = tag_accum)]
  447.     else:
  448.         result[-1].post_tags.extend(tag_accum)
  449.     return result
  450.  
  451. empty_tags = ('param', 'img', 'area', 'br', 'basefont', 'input', 'base', 'meta', 'link', 'col')
  452. block_level_tags = ('address', 'blockquote', 'center', 'dir', 'div', 'dl', 'fieldset', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'isindex', 'menu', 'noframes', 'noscript', 'ol', 'p', 'pre', 'table', 'ul')
  453. block_level_container_tags = ('dd', 'dt', 'frameset', 'li', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr')
  454.  
  455. def flatten_el(el, include_hrefs, skip_tag = False):
  456.     if not skip_tag:
  457.         if el.tag == 'img':
  458.             yield ('img', el.attrib['src'], start_tag(el))
  459.         else:
  460.             yield start_tag(el)
  461.     
  462.     if el.tag in empty_tags and not (el.text) and not len(el) and not (el.tail):
  463.         return None
  464.     
  465.     start_words = split_words(el.text)
  466.     for word in start_words:
  467.         yield cgi.escape(word)
  468.     
  469.     for child in el:
  470.         for item in flatten_el(child, include_hrefs = include_hrefs):
  471.             yield item
  472.         
  473.     
  474.     if el.tag == 'a' and el.attrib.get('href') and include_hrefs:
  475.         yield ('href', el.attrib['href'])
  476.     
  477.     if not skip_tag:
  478.         yield end_tag(el)
  479.         end_words = split_words(el.tail)
  480.         for word in end_words:
  481.             yield cgi.escape(word)
  482.         
  483.     
  484.  
  485.  
  486. def split_words(text):
  487.     if not text or not text.strip():
  488.         return []
  489.     
  490.     words = [ w + ' ' for w in text.strip().split() ]
  491.     return words
  492.  
  493. start_whitespace_re = re.compile('^[ \\t\\n\\r]')
  494.  
  495. def start_tag(el):
  496.     return ''.join % ([], []([ ' %s="%s"' % (name, cgi.escape(value, True)) for name, value in el.attrib.items() ]))
  497.  
  498.  
  499. def end_tag(el):
  500.     if el.tail and start_whitespace_re.search(el.tail):
  501.         extra = ' '
  502.     else:
  503.         extra = ''
  504.     return '</%s>%s' % (el.tag, extra)
  505.  
  506.  
  507. def is_word(tok):
  508.     return not tok.startswith('<')
  509.  
  510.  
  511. def is_end_tag(tok):
  512.     return tok.startswith('</')
  513.  
  514.  
  515. def is_start_tag(tok):
  516.     if tok.startswith('<'):
  517.         pass
  518.     return not tok.startswith('</')
  519.  
  520.  
  521. def fixup_ins_del_tags(html):
  522.     doc = parse_html(html, cleanup = False)
  523.     _fixup_ins_del_tags(doc)
  524.     html = serialize_html_fragment(doc, skip_outer = True)
  525.     return html
  526.  
  527.  
  528. def serialize_html_fragment(el, skip_outer = False):
  529.     html = etree.tostring(el, method = 'html', encoding = _unicode)
  530.     if skip_outer:
  531.         html = html[html.find('>') + 1:]
  532.         html = html[:html.rfind('<')]
  533.         return html.strip()
  534.     else:
  535.         return html
  536.  
  537.  
  538. def _fixup_ins_del_tags(doc):
  539.     for tag in [
  540.         'ins',
  541.         'del']:
  542.         for el in doc.xpath('descendant-or-self::%s' % tag):
  543.             if not _contains_block_level_tag(el):
  544.                 continue
  545.             
  546.             _move_el_inside_block(el, tag = tag)
  547.             el.drop_tag()
  548.         
  549.     
  550.  
  551.  
  552. def _contains_block_level_tag(el):
  553.     if el.tag in block_level_tags or el.tag in block_level_container_tags:
  554.         return True
  555.     
  556.     for child in el:
  557.         if _contains_block_level_tag(child):
  558.             return True
  559.             continue
  560.     
  561.     return False
  562.  
  563.  
  564. def _move_el_inside_block(el, tag):
  565.     for child in el:
  566.         if _contains_block_level_tag(child):
  567.             break
  568.             continue
  569.     else:
  570.         import sys
  571.         children_tag = etree.Element(tag)
  572.         children_tag.text = el.text
  573.         el.text = None
  574.         el[:] = [
  575.             children_tag]
  576.         return None
  577.     for child in list(el):
  578.         if _contains_block_level_tag(child):
  579.             _move_el_inside_block(child, tag)
  580.             if child.tail:
  581.                 tail_tag = etree.Element(tag)
  582.                 tail_tag.text = child.tail
  583.                 child.tail = None
  584.                 el.insert(el.index(child) + 1, tail_tag)
  585.             
  586.         child.tail
  587.         child_tag = etree.Element(tag)
  588.         el.replace(child, child_tag)
  589.         child_tag.append(child)
  590.     
  591.     if el.text:
  592.         text_tag = etree.Element(tag)
  593.         text_tag.text = el.text
  594.         el.text = None
  595.         el.insert(0, text_tag)
  596.     
  597.  
  598.  
  599. def _merge_element_contents(el):
  600.     parent = el.getparent()
  601.     if not el.text:
  602.         pass
  603.     text = ''
  604.     if el.tail:
  605.         if not len(el):
  606.             text += el.tail
  607.         elif el[-1].tail:
  608.             el[-1].tail += el.tail
  609.         else:
  610.             el[-1].tail = el.tail
  611.     
  612.     index = parent.index(el)
  613.     if text:
  614.         if index == 0:
  615.             previous = None
  616.         else:
  617.             previous = parent[index - 1]
  618.         if previous is None:
  619.             if parent.text:
  620.                 parent.text += text
  621.             else:
  622.                 parent.text = text
  623.         elif previous.tail:
  624.             previous.tail += text
  625.         else:
  626.             previous.tail = text
  627.     
  628.     parent[index:index + 1] = el.getchildren()
  629.  
  630.  
  631. class InsensitiveSequenceMatcher(difflib.SequenceMatcher):
  632.     threshold = 2
  633.     
  634.     def get_matching_blocks(self):
  635.         size = min(len(self.b), len(self.b))
  636.         threshold = min(self.threshold, size / 4)
  637.         actual = difflib.SequenceMatcher.get_matching_blocks(self)
  638.         return _[1]
  639.  
  640.  
  641. if __name__ == '__main__':
  642.     from lxml.html import _diffcommand
  643.     _diffcommand.main()
  644.  
  645.